package services

import (
	"context"
	"fmt"
	"gitee.com/igolang/imoney/internal/app/system/entities"
	"gitee.com/igolang/imoney/internal/app/system/entities/constants"
	"gitee.com/igolang/imoney/internal/app/system/keys"
	"gitee.com/igolang/imoney/internal/app/system/tools"
	"gitee.com/igolang/imoney/internal/common/httpResp"
	"gitee.com/igolang/imoney/internal/common/tools/captcha"
	"gitee.com/igolang/imoney/internal/core/client"
	"gitee.com/igolang/imoney/internal/core/logger"
	"gitee.com/igolang/imoney/internal/models/dao"
	"gitee.com/igolang/imoney/internal/models/model"
	"gitee.com/igolang/pkg/xcfg"
	"gitee.com/igolang/pkg/xgorm"
	"gitee.com/igolang/pkg/xjson"
	"gitee.com/igolang/pkg/xjwt"
	"gitee.com/igolang/pkg/xset"
	"gitee.com/igolang/pkg/xtime"
	"gitee.com/igolang/pkg/xutils"
	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v4"
	"github.com/pkg/errors"
	uuid "github.com/satori/go.uuid"
	"github.com/spf13/cast"
	"gorm.io/gorm"
	"log"
	"net/http"
	"reflect"
	"slices"
	"time"
)

var (
	captchaInstance = captcha.NewCaptcha(keys.Captcha)
)

func (s *user) Captcha(ctx *gin.Context) (result map[string]any, err error) {
	result = map[string]any{
		"captchaEnabled": Config.CaptchaEnabled(),
	}
	id, b64s, _, err := captchaInstance.Generate()
	result["img"] = b64s
	result["uuid"] = id

	return result, err
}

func (s *user) Login(ctx *gin.Context) (string, error) {
	var req entities.LoginUserReq
	var err error
	if err = ctx.ShouldBindJSON(&req); err != nil {
		return "", err
	}
	row, token, err := s.doLogin(req)
	defer func() {
		loginResult := &entities.LoginResult{
			T:      "login",
			Status: constants.OkStatus,
			Msg:    "登录成功",
		}
		if err != nil {
			loginResult.Status = "1"
			loginResult.Msg = err.Error()
		}
		if token != nil {
			loginResult.Id = token.md5Str
		}
		Logininfor.AddUserLoginLog(ctx, &req, loginResult)
	}()

	if err != nil {
		// 记录错误次数
		rc := client.Redis()
		errKey := keys.UserLoginErr(req.Username)
		rc.IncrBy(ctx, errKey.Key, 1)
		if rc.TTL(ctx, errKey.Key).Val() < 0 {
			expired := xcfg.GetInt("system.user.password.lockTime")
			rc.Expire(ctx, errKey.Key, time.Duration(expired)*time.Minute)
		}
		return "", err
	}
	row.LoginIP = xutils.ClientIp(ctx.Request)
	row.LoginDate = xtime.Time(time.Now())
	// 更新用户
	log.Println(dao.SysUser.Updates(row))
	logger.Log().Debug("用户登录，更新缓存", "user", row, "err", s.LoginUserClean(row.UserID))

	return token.token, err
}

func (s *user) Unlock(ctx *gin.Context) error {
	name := ctx.Param("name")
	rc := client.Redis()
	errKey := keys.UserLoginErr(name)
	return rc.Del(context.Background(), errKey.Key).Err()
}

func (s *user) isLocking(name string) error {
	rc := client.Redis()
	errKey := keys.UserLoginErr(name)
	maxRetryCount := xcfg.GetInt("system.user.password.maxRetryCount")
	lockTime := xcfg.GetInt("system.user.password.lockTime")
	if count, _ := rc.Get(context.Background(), errKey.Key).Int(); count >= maxRetryCount {
		return fmt.Errorf("密码输入错误%d次，帐户锁定%d分钟", maxRetryCount, lockTime)
	}

	return nil
}

func (s *user) doLogin(req entities.LoginUserReq) (*model.SysUser, *loginToken, error) {
	var err error
	// 是否锁定
	err = s.isLocking(req.Username)
	if err != nil {
		return nil, nil, err
	}

	// 验证验证码
	if Config.CaptchaEnabled() {
		if !captchaInstance.Verify(req.Uuid, req.Code, true) {
			return nil, nil, errors.New("验证码错误")
		}
	}
	// 验证用户
	row, err := dao.SysUser.Where(dao.SysUser.UserName.Eq(req.Username)).Take()
	if err != nil || row == nil {
		return nil, nil, httpResp.ErrorCode(httpResp.ErrCodeUserNotExists)
	}
	if row.Status != constants.OkStatus {
		return nil, nil, httpResp.ErrorCode(httpResp.ErrCodeInvalidUser)
	}

	// 验证密码
	if !xutils.PasswordVerify(req.Password, row.Password) {
		return nil, nil, httpResp.ErrorCode(httpResp.ErrCodeInvalidPassword)
	}

	// 生成token
	token, err := s.CreateToken(row.UserID)
	if err != nil {
		logger.Log().Error("用户登录生成token错误", "err", err)
		return nil, nil, httpResp.ErrorCode(httpResp.ErrCodeInvalidPassword)
	}

	// 将token存储到集合中
	rc := client.Redis()
	tokenKey := keys.UserToken(token.md5Str)
	ctx := context.TODO()
	if err = rc.HSet(context.TODO(), tokenKey.Key, "uid", row.UserID).Err(); err != nil {
		return nil, nil, err
	}

	rc.Expire(ctx, tokenKey.Key, token.expired)

	return row, token, err
}

type loginToken struct {
	token   string
	md5Str  string
	expired time.Duration
}

func (s *user) CreateToken(uid int64) (*loginToken, error) {
	secret := xcfg.GetString("system.token.secret")
	expired := xcfg.GetDuration("system.token.expired")
	if expired.Seconds() == 0 {
		expired = constants.TokenExpired
	}
	expt := time.Now().Add(expired)
	tokenStr := fmt.Sprintf("%d:%d", uid, expt.Unix())
	md5Token := xutils.Md5(xutils.HS256([]byte(tokenStr), []byte(secret)))
	j := xjwt.New([]byte(secret))
	claims := j.CreateClaims(nil, jwt.RegisteredClaims{
		Subject:   md5Token,
		ExpiresAt: jwt.NewNumericDate(expt),
		ID:        uuid.NewV4().String(),
	})
	token, err := j.CreateToken(claims)
	if err != nil {
		return nil, err
	}

	return &loginToken{token: token, md5Str: md5Token, expired: expired}, nil
}

func (s *user) ParseToken(token string) (int64, error) {
	// 解密
	md5Token := s.parseMd5Token(token)
	if md5Token == "" {
		return 0, httpResp.ErrorCode(httpResp.ErrCodeInvalidToken)
	}

	rc := client.Redis()
	tokenKey := keys.UserToken(md5Token)
	return rc.HGet(context.TODO(), tokenKey.Key, "uid").Int64()
}

func (s *user) parseMd5Token(token string) string {
	secret := xcfg.GetString("system.token.secret")
	j := xjwt.New([]byte(secret))
	claims, err := j.ParseToken(token)
	if err != nil {
		logger.Log().Error("parse token err", "err", err)
		return ""
	}
	return claims.Subject
}

func (s *user) Logout(ctx *gin.Context) error {
	token := ctx.GetString(constants.LoginTokenKey)
	if token == "" {
		return nil
	}
	md5Token := s.parseMd5Token(token)
	if md5Token == "" {
		return nil
	}
	rc := client.Redis()
	tokenKey := keys.UserToken(md5Token)
	err := rc.Del(context.TODO(), tokenKey.Key).Err()
	loginResult := &entities.LoginResult{
		Id:     md5Token,
		T:      "logout",
		Status: "0",
		Msg:    "退出成功",
	}
	userInfo := tools.GetUserInfo(ctx)
	defer s.LoginUserClean(userInfo.User.UserID)
	if err != nil {
		loginResult.Status = "1"
		loginResult.Msg = err.Error()
	}
	Logininfor.AddUserLoginLog(ctx, &entities.LoginUserReq{
		Username: userInfo.User.UserName,
	}, loginResult)

	return err

}

func (s *user) Quit(ctx *gin.Context) error {
	id := ctx.Param("id")
	if id == "" {
		return httpResp.ErrorCode(httpResp.ErrCodeInvalidParams)
	}
	userInfo := tools.GetUserInfo(ctx)
	tokenKey := keys.UserToken(id)
	rc := client.Redis()
	uid, err := rc.HGet(ctx, tokenKey.Key, "uid").Int64()
	if err != nil {
		return err
	}

	if uid == constants.SuperAdminId && !userInfo.IsSuperAdmin() {
		return httpResp.ErrorCode(http.StatusForbidden)
	}
	defer s.LoginUserClean(uid)
	return rc.Del(ctx, tokenKey.Key).Err()
}

func (s *user) LoginUser(token string) (*entities.LoginUserInfo, error) {
	uid, err := s.ParseToken(token)
	if err != nil {
		return nil, err
	}
	rc := client.Redis()
	userKey := keys.LoginUser(uid)
	userInfo := &entities.LoginUserInfo{}
	userValue, _ := rc.Get(context.Background(), userKey.Key).Bytes()
	if len(userValue) > 0 {
		xjson.Unmarshal(userValue, userInfo)
		if userInfo.User.UserID == uid {
			return userInfo, nil
		}
	}
	row, err := dao.SysUser.Where(
		dao.SysUser.Status.Eq(constants.OkStatus),
		dao.SysUser.UserID.Eq(uid)).Take()
	if err != nil {
		return nil, err
	}

	userInfo.User.SysUser = row
	// 获取部门
	deptRow, _ := dao.SysDept.Where(
		dao.SysDept.DeptID.Eq(row.DeptID),
		dao.SysDept.Status.Eq(constants.OkStatus),
	).Take()
	if deptRow == nil {
		deptRow = &model.SysDept{}
	}
	userInfo.User.Dept = deptRow
	// 获取角色
	roles, err := dao.SysRole.Distinct(dao.SysRole.ALL).
		LeftJoin(dao.SysUserRole, dao.SysUserRole.RoleID.EqCol(dao.SysRole.RoleID)).
		Where(
			dao.SysRole.Status.Eq(constants.OkStatus),
			dao.SysUserRole.UserID.Eq(uid)).Find()
	if err != nil {
		return nil, err
	}
	userInfo.User.Roles = roles
	roleId := make([]int64, 0)
	userInfo.Roles = make([]string, 0)
	for _, roleRow := range roles {
		userInfo.Roles = append(userInfo.Roles, roleRow.RoleKey)
		roleId = append(roleId, roleRow.RoleID)
	}
	// 获取权限
	if row.UserID == constants.SuperAdminId {
		userInfo.Permissions = []string{constants.AllPermission}
	} else {
		userInfo.Permissions = make([]string, 0)
		menus, err := dao.SysRoleMenu.Distinct(dao.SysRoleMenu.MenuID).Where(dao.SysRoleMenu.RoleID.In(roleId...)).Find()
		if err != nil {
			return nil, err
		}
		menuId := make([]int64, 0)
		for _, v := range menus {
			menuId = append(menuId, v.MenuID)
		}
		perms, err := dao.SysMenu.Select(dao.SysMenu.Perms).Where(dao.SysMenu.MenuID.In(menuId...)).Find()
		if err != nil {
			return nil, err
		}
		sets := xset.New(userInfo.Permissions...)
		for _, v := range perms {
			sets.Add(v.Perms)
		}
		userInfo.Permissions = sets.Values()
	}

	// 设置缓存
	expired := xcfg.GetDuration("system.token.expired")
	if expired.Seconds() == 0 {
		expired = constants.TokenExpired
	}

	b, _ := xjson.Marshal(userInfo)
	if err = rc.SetNX(context.Background(), userKey.Key, b, expired).Err(); err != nil {
		return nil, err
	}
	return userInfo, nil
}

func (s *user) LoginUserClean(uid int64) error {
	rc := client.Redis()
	userKey := keys.LoginUser(uid)
	return rc.Del(context.Background(), userKey.Key).Err()
}

func (s *user) UpdateCallback() xgorm.Callback {
	return xgorm.Callback{
		Name:      "sysUserUpdate",
		Action:    "update",
		SortName:  "after",
		SortValue: "*",
		Callback: func(db *gorm.DB) {
			if db.Statement.Schema == nil {
				return
			}
			if !slices.Contains([]string{dao.SysUser.TableName(), dao.SysUser.TableName()}, db.Statement.Schema.Table) {
				return
			}
			// 获取主键
			field := db.Statement.Schema.PrioritizedPrimaryField
			if field == nil {
				return
			}

			switch db.Statement.ReflectValue.Kind() {
			case reflect.Slice, reflect.Array:
				for i := 0; i < db.Statement.ReflectValue.Len(); i++ {
					if fieldValue, isZero := field.ValueOf(db.Statement.Context, db.Statement.ReflectValue.Index(i)); !isZero {
						s.LoginUserClean(cast.ToInt64(fieldValue))
					}
				}
			case reflect.Struct:
				if fieldValue, isZero := field.ValueOf(db.Statement.Context, db.Statement.ReflectValue); !isZero {
					s.LoginUserClean(cast.ToInt64(fieldValue))
				}
			}
		},
	}
}
